﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace TorrentProxy
{
	public class Proxy : IDisposable
	{
		private HttpListener _listener;
		private Task _task;
		private CancellationTokenSource _cancelTokenSource;
		private uint _port;
		private static ThreadSafeRandom random = new ThreadSafeRandom();

		private List<LogItem> _log;

		private LeechingMode _leechingMode;
		public LeechingMode LeechingMode
		{
			get
			{
				return _leechingMode;
			}
			set { _leechingMode = value; }
		}

		private float _leechingModeOverrideMinRatio;
		public float LeechingModeOverrideMinRatio
		{
			get
			{
				return Interlocked.Exchange(ref _leechingModeOverrideMinRatio, _leechingModeOverrideMinRatio);
			}
			set
			{
				Interlocked.Exchange(ref _leechingModeOverrideMinRatio, value);                
			}
		}

		private float _leechingModeOverrideMaxRatio;
		public float LeechingModeOverrideMaxRatio
		{
			get
			{
				return Interlocked.Exchange(ref _leechingModeOverrideMaxRatio, _leechingModeOverrideMaxRatio);
			}
			set
			{
				Interlocked.Exchange(ref _leechingModeOverrideMaxRatio, value);
			}
		}

		private float _leechingModeIncreaseMinRatio;
		public float LeechingModeIncreaseMinRatio
		{
			get
			{
				return Interlocked.Exchange(ref _leechingModeIncreaseMinRatio, _leechingModeIncreaseMinRatio);
			}
			set
			{
				Interlocked.Exchange(ref _leechingModeIncreaseMinRatio, value);
			}
		}

		private float _leechingModeIncreaseMaxRatio;
		public float LeechingModeIncreaseMaxRatio
		{
			get
			{
				return Interlocked.Exchange(ref _leechingModeIncreaseMaxRatio, _leechingModeIncreaseMaxRatio);
			}
			set
			{
				Interlocked.Exchange(ref _leechingModeIncreaseMaxRatio, value);
			}
		}

		private float _seedingModeMinRatio;
		public float SeedingModeMinRatio
		{
			get
			{
				return Interlocked.Exchange(ref _seedingModeMinRatio, _seedingModeMinRatio);
			}
			set
			{
				Interlocked.Exchange(ref _seedingModeMinRatio, value);
			}
		}

		private float _seedingModeMaxRatio;
		public float SeedingModeMaxRatio
		{
			get
			{
				return Interlocked.Exchange(ref _seedingModeMaxRatio, _seedingModeMaxRatio);
			}
			set
			{
				Interlocked.Exchange(ref _seedingModeMaxRatio, value);
			}
		}

		private long _decreaseDownloadedTraffic;
		public bool DecreaseDownloadedTraffic
		{
			get { return Convert.ToBoolean(Interlocked.Read(ref _decreaseDownloadedTraffic)); }
			set { Interlocked.Exchange(ref _decreaseDownloadedTraffic, Convert.ToInt64(value)); }
		}

		private float _decreaseDownloadedTrafficRatio;
		public float DecreaseDownloadedTrafficRatio
		{
			get
			{
				return Interlocked.Exchange(ref _decreaseDownloadedTrafficRatio, _decreaseDownloadedTrafficRatio);
			}
			set
			{
				Interlocked.Exchange(ref _decreaseDownloadedTrafficRatio, value);
			}
		}


		public bool IsRunning
		{
			get
			{
				return _task == null ? false : _task.Status == TaskStatus.Running;
			}
		}

		public ReadOnlyCollection<LogItem> Log
		{
			get { return _log.AsReadOnly(); }
		}
		

		public Proxy(uint port)
		{
			_log = new List<LogItem>();
			_listener = new HttpListener();
			_cancelTokenSource = new CancellationTokenSource();
			_task = new Task(() => ProxyFunc(_cancelTokenSource.Token), _cancelTokenSource.Token, TaskCreationOptions.LongRunning);
			_port = port;
			_leechingMode = LeechingMode.Increase;
			_leechingModeOverrideMinRatio = 0;
			_leechingModeOverrideMaxRatio = 0;
			_leechingModeIncreaseMinRatio = 1;
			_leechingModeIncreaseMaxRatio = 1;
			_seedingModeMinRatio = 1;
			_seedingModeMaxRatio = 1;
		}

		public void Start()
		{
			if (_disposed)
				throw new ObjectDisposedException("this");       
			_task.Start();
		}

		public void Stop()
		{
			if (_disposed)
				throw new ObjectDisposedException("this");

			_cancelTokenSource.Cancel();
			_task.Wait();
		}

		private void ProxyFunc(CancellationToken token)
		{
			_listener.Prefixes.Add(String.Format("http://127.0.0.1:{0}/", _port));
			_listener.Start();
			while (!token.IsCancellationRequested)
			{
				IAsyncResult result = _listener.BeginGetContext(ProcessRequestCallback, _listener);
				WaitHandle.WaitAny(new[] { token.WaitHandle, result.AsyncWaitHandle });
			}
			_listener.Stop();
		}

		private void ProcessRequestCallback(IAsyncResult result)
		{
			HttpWebRequest psRequest;                   // Request to send to remote web server
			HttpWebResponse psResponse;                 // Response from remote web server         
			List<byte> requestBody = new List<byte>();  // Byte array to hold the request's body
			List<byte> responseBody = new List<byte>(); // Byte array to hold the response's body
			byte[] buffer;
			LogItem logItem = new LogItem();
			logItem.Time = DateTime.UtcNow;

			try
			{
				int downloadedBytes;
				int uploadedBytes;
				HttpListener listener = (HttpListener)result.AsyncState;
				// Call EndGetContext to complete the asynchronous operation.
				HttpListenerContext context = listener.EndGetContext(result);
				string psUri = ReplaceUri(context.Request.Url, context.Request.Headers["Host"], out downloadedBytes,
				                          out uploadedBytes);
				psRequest = (HttpWebRequest)WebRequest.Create(psUri);
				psRequest.Method = context.Request.HttpMethod;
				psRequest.ProtocolVersion = context.Request.ProtocolVersion;
				psRequest.UserAgent = context.Request.UserAgent;
				psRequest.ContentType = context.Request.ContentType;
				psRequest.ContentLength = context.Request.ContentLength64;
				foreach (string key in context.Request.Headers.Keys)
				{
					switch (key)
					{
						case "Proxy-Connection":
						case "Connection":
							psRequest.KeepAlive = (context.Request.Headers[key].ToLower() == "keep-alive") ? true : false;
							break;

						case "Content-Length":
							psRequest.ContentLength = context.Request.ContentLength64;
							break;

						case "Content-Type":
							psRequest.ContentType = context.Request.Headers[key];
							break;

						case "Accept":
							psRequest.Accept = context.Request.Headers[key];
							break;

						case "User-Agent":
						case "Host":
							break;

						default:
							psRequest.Headers.Add(key, context.Request.Headers[key]);
							break;
					}
				}
				logItem.Tracker = context.Request.Url.Host;
				logItem.Downloaded = downloadedBytes;
				logItem.Uploaded = uploadedBytes;

				// Transfer the body data from HttpListenerContext Request into HttpWebRequest
				buffer = new byte[256];
				Stream instream = context.Request.InputStream;
				try
				{
					int incount = instream.Read(buffer, 0, buffer.Length);
					while (incount > 0)
					{
						byte[] temp = new byte[incount];
						Array.Copy(buffer, temp, incount);
						requestBody.AddRange(temp);

						psRequest.GetRequestStream().Write(buffer, 0, incount);
						incount = instream.Read(buffer, 0, buffer.Length);
					}
				}
				finally
				{
					instream.Close();
				}

				//TODO: Modify psRequest object here if desired.

				// Send the request to the remote server and get the response
				try
				{
					psResponse = (HttpWebResponse)psRequest.GetResponse();
				}
				catch (Exception ex)
				{
					logItem.Status = LogItemStatus.Failure;
					logItem.ErrorText = ex.Message;
					_log.Add(logItem);
					return;
				}

				logItem.Status = LogItemStatus.OK;

				// Convert HttpWebResponse to HttpListenerContext response to send back to user
				foreach (string key in psResponse.Headers.Keys)
				{
					try
					{
						switch (key)
						{
							case "Transfer-Encoding":
								context.Response.SendChunked = (psResponse.Headers[key].ToLower() == "chunked") ? true : false;
								break;

							case "Content-Length":
								context.Response.ContentLength64 = psResponse.ContentLength;
								break;

							case "Content-Type":
								context.Response.ContentType = psResponse.Headers[key];
								break;

							case "Keep-Alive":
								context.Response.KeepAlive = true;
								break;
							
							default:
								context.Response.Headers.Add(key, psResponse.Headers[key]);
								break;
						}
					}
					catch (Exception ex)
					{
						//TODO: log erroes						
					}
				}

				// Transfer the body data from the HttpWebResponse response
				buffer = new byte[256];
				Stream outstream = psResponse.GetResponseStream();
				if (outstream != null)
				{
					int outcount = outstream.Read(buffer, 0, buffer.Length);
					while (outcount > 0)
					{
						byte[] temp = new byte[outcount];
						Array.Copy(buffer, temp, outcount);
						responseBody.AddRange(temp);

						context.Response.OutputStream.Write(buffer, 0, outcount);
						outcount = outstream.Read(buffer, 0, buffer.Length);
					}

					outstream.Close();
				}
				
				//context.Response.OutputStream.Close();
				context.Response.Close();
			}
			catch (Exception ex)
			{
				//TODO: log error
				logItem.Status = LogItemStatus.Failure;
				logItem.ErrorText = ex.Message;
			}			
			_log.Add(logItem);
		}

		private String ReplaceUri(Uri uri, String host, out int downloadedBytes, out int uploadedBytes)
		{
			int uploaded;
			int downloaded;
			String url = uri.OriginalString;
			if (!String.IsNullOrEmpty(host))
			{
				url = url.Replace(uri.Host, host);
			}
			url = url.Replace(string.Format(":{0}", _port), "");

			Regex regex = new Regex(@"uploaded=(\d+)&downloaded=(\d+)&");
			Match match = regex.Match(url);
			if (match.Success)
			{
				uploaded = Int32.Parse(match.Groups[1].Value);
				downloaded = Int32.Parse(match.Groups[2].Value);

				if (downloaded > 0)
				{
					switch (_leechingMode)
					{
						case LeechingMode.Increase:
							uploaded = (int)(uploaded * random.Next(_leechingModeIncreaseMinRatio, _leechingModeIncreaseMaxRatio));
							break;
						case LeechingMode.Override:
							uploaded = (int)(downloaded * random.Next(_leechingModeOverrideMinRatio, _leechingModeOverrideMaxRatio));
							break;
					}
				}
				else if (uploaded > 0)
				{
					uploaded = (int)(uploaded * random.Next(_seedingModeMinRatio, _seedingModeMaxRatio));
				}

				if (_decreaseDownloadedTraffic == 1 && downloaded > 0)
				{
					downloaded -= (int)(downloaded * _decreaseDownloadedTrafficRatio);
				}
				
				url = regex.Replace(url, String.Format(@"uploaded={0}&downloaded={1}&", uploaded, downloaded));
				downloadedBytes = downloaded;
				uploadedBytes = uploaded;
			}
			else
			{
				downloadedBytes = 0;
				uploadedBytes = 0;
			}
			return url;
		}

		private Boolean _disposed;

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, 
		/// or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="managed"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
		protected virtual void Dispose(Boolean managed)
		{
			if (!_disposed)
			{
				if (managed)
				{
					_cancelTokenSource.Dispose();
					_task.Dispose();
					_listener.Close();
				}

				_disposed = true;
			}
		}
	}
}
